package lol.clann.minecraft.springboot.plugin.bukkit.impl;

import com.ruoshui.utils.java.exception.ExceptionUtils;
import lol.clann.minecraft.springboot.SpringBootPlugin;
import lol.clann.minecraft.springboot.api.annotation.command.Command;
import lol.clann.minecraft.springboot.api.annotation.command.CommandDefine;
import lol.clann.minecraft.springboot.api.annotation.command.Optional;
import lol.clann.minecraft.springboot.api.annotation.command.Sender;
import lol.clann.minecraft.springboot.api.bukkit.utils.EntityUtils;
import lol.clann.minecraft.springboot.api.bukkit.utils.JSUtils;
import lol.clann.minecraft.springboot.api.bukkit.utils.PlayerUtils;
import lol.clann.minecraft.springboot.api.bukkit.utils.PluginUtils;
import lol.clann.minecraft.springboot.api.context.PluginContext;
import lol.clann.minecraft.springboot.api.context.SpringContext;
import lol.clann.minecraft.springboot.api.model.LazyField;
import lol.clann.minecraft.springboot.api.spi.extend.JSScriptContextProvider;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
import relocate.com.google.common.cache.CacheBuilder;
import relocate.com.google.common.cache.CacheLoader;
import relocate.com.google.common.cache.LoadingCache;

import javax.annotation.PostConstruct;
import javax.script.*;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;

/**
 * 提供js脚本支持
 *
 * @author pyz
 * @date 2019/5/22 12:18 AM
 */
@Slf4j
@CommandDefine(name = "springboot")
public class JSUtilsImpl implements JSUtils {

    @Autowired
    private PlayerUtils playerUtils;
    @Autowired
    private PluginUtils pluginUtils;
    @Autowired
    private EntityUtils entityUtils;
    @Getter
    private ScriptEngine scriptEngine;
    private LoadingCache<String, CompiledScript> scriptFileCache;
    private LoadingCache<String, CompiledScript> scriptCache;
    private File scriptDir;
    private Map<String, Object> defaultBindings = new HashMap<>();

    @PostConstruct
    private void init() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        ScriptEngineFactory factory = null;
        for (ScriptEngineFactory f : new ScriptEngineManager(System.class.getClassLoader()).getEngineFactories()) {
            LOGGER.info("available script engine:" + f.getEngineName());
            if (f.getEngineName().equalsIgnoreCase("Oracle Nashorn")) {
                factory = f;
                break;
            }
        }
        if (factory == null) {
            throw new NullPointerException();
        }
        String[] stringArray = new String[]{"-doe", "--global-per-engine"};
//        contigo thermos kcauldron等核心上这个强转会抛异常,改成反射做
//        scriptEngine = ((NashornScriptEngineFactory) factory).getScriptEngine(stringArray);
        Method getScriptEngine = factory.getClass().getDeclaredMethod("getScriptEngine", new Class[]{String[].class});
        scriptEngine = (ScriptEngine) getScriptEngine.invoke(factory, new Object[]{stringArray});
        scriptFileCache = CacheBuilder
                .newBuilder()
                .expireAfterAccess(60, TimeUnit.SECONDS)
                .build(new CacheLoader<String, CompiledScript>() {
                    @Override
                    public CompiledScript load(String scriptPath) throws Exception {
                        File file = new File(scriptDir, scriptPath);
                        if (!file.exists() || !file.isFile()) {
                            throw new IllegalArgumentException("找不到指定的脚本,请检查文件是否存在:" + file.getAbsolutePath());
                        }
                        StringBuilder sb = new StringBuilder();
                        String line;
                        BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8));
                        while ((line = reader.readLine()) != null) {
                            sb.append(line).append(System.lineSeparator());
                        }
                        reader.close();
                        return ((Compilable) scriptEngine).compile(sb.toString());
                    }
                });
        scriptCache = CacheBuilder
                .newBuilder()
                .expireAfterAccess(60, TimeUnit.SECONDS)
                .build(new CacheLoader<String, CompiledScript>() {
                    @Override
                    public CompiledScript load(String script) throws Exception {
                        return ((Compilable) scriptEngine).compile(script);
                    }
                });

        File dir = new File(PluginContext.getPlugin(SpringBootPlugin.id).getDataFolder().toFile(), "javascript");
        if (!dir.exists()) {
            dir.mkdirs();
        }
        scriptDir = dir;

        defaultBindings.putIfAbsent("logger", LOGGER);
        defaultBindings.putIfAbsent("random", ThreadLocalRandom.current());
        defaultBindings.putIfAbsent("playerUtils", playerUtils);
        defaultBindings.putIfAbsent("entityUtils", entityUtils);
        defaultBindings.putIfAbsent("server", Bukkit.getServer());
        defaultBindings.putIfAbsent("scheduler", Bukkit.getScheduler());
        defaultBindings.putIfAbsent("plugin", pluginUtils.getSelf());
    }

    @Command(showArgs = "reloadJavaScript [scriptPath]", des = "重载指定/全部脚本")
    private void reloadJavaScript(@Sender CommandSender sender, @Optional String scriptPath) {
        if (scriptPath != null) {
            scriptFileCache.invalidate(scriptPath);
            sender.sendMessage(ChatColor.translateAlternateColorCodes('&', "重载脚本:" + scriptPath));
        } else {
            List<String> scriptPaths = new ArrayList<>(scriptFileCache.asMap().keySet());
            scriptPaths.sort(String::compareTo);
            for (String path : scriptPaths) {
                scriptFileCache.invalidate(path);
                sender.sendMessage(ChatColor.translateAlternateColorCodes('&', "重载脚本:" + path));
            }
        }
    }

    @Override
    public CompiledScript compileScriptFile(String file) {
        try {
            return scriptFileCache.get(file);
        } catch (ExecutionException e) {
            ExceptionUtils.throwException(e);
            return null;
        }
    }

    @Override
    public CompiledScript compileScript(String javascript) {
        try {
            return scriptCache.get(javascript);
        } catch (ExecutionException e) {
            ExceptionUtils.throwException(e);
            return null;
        }
    }

    /**
     * 脚本中常驻变量
     * logger       lol.clann.minecraft.springboot.api.bukkit.utils.JSUtils
     * random       java.util.concurrent.ThreadLocalRandom
     * playerUtils  lol.clann.minecraft.springboot.api.bukkit.utils.PlayerUtils
     * server       org.bukkit.Server
     * scheduler    org.bukkit.scheduler.BukkitScheduler
     * plugin       org.bukkit.plugin.java.JavaPlugin
     *
     * @param scriptPath
     * @param bindings
     * @return
     */
    @Override
    public Object evelScriptFile(String scriptPath, Map<String, Object> bindings) {
        try {
            fillDefaultBindings(bindings);
            Map<String, Object> paramMap = new HashMap<>();
            paramMap.put("context", bindings);
            return compileScriptFile(scriptPath).eval(new SimpleBindings(paramMap));
        } catch (ScriptException e) {
            ExceptionUtils.throwException(e);
            return null;
        }
    }

    @Override
    public Object evelScript(String script, Map<String, Object> bindings) {
        try {
            fillDefaultBindings(bindings);
            Map<String, Object> paramMap = new HashMap<>();
            paramMap.put("context", bindings);
            return compileScript(script).eval(new SimpleBindings(paramMap));
        } catch (ScriptException e) {
            ExceptionUtils.throwException(e);
            return null;
        }
    }

    private void fillDefaultBindings(Map<String, Object> bindings) {
        defaultBindings.forEach((k, v) -> {
            bindings.putIfAbsent(k, v);
        });
    }

    @Component
    public static class JSScriptContextProviderHook implements BeanPostProcessor {

        private LazyField<JSUtilsImpl> jsUtils = LazyField.of(() -> SpringContext.getBean(JSUtilsImpl.class));

        @Override
        public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
            if (bean instanceof JSScriptContextProvider) {
                Map<String, Object> extendDefaultBindings = ((JSScriptContextProvider) bean).provide();
                if (extendDefaultBindings != null) {
                    jsUtils.get().defaultBindings.putAll(((JSScriptContextProvider) bean).provide());
                }
            }
            return bean;
        }
    }
}
